package mx.uach.proyecto.compiladores;

//TODO JavaDoc no olviden poner la gramatica en el java Doc dela clase
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.StringTokenizer;

/**
 * Gramatica del analizador de producciones gramaticales.
 *
 * <ul>
 * <li> prog → conjuProds </li>
 * <li> conjuProds → conjuProds | produ </li>
 * <li> produ → variable ::= expr; </li>
 * <li> expr → expr ALTER term | term </li>
 * <li> term → term & factor | factor </li>
 * <li> factor → {expr} | [expr] | primario </li>
 * <li> primario → (expr) | variable | terminal </li>
 * </ul>
 *
 * @author Jesus Jose Garcia Pardo 271305
 * @author Jorge Orozco Terrazas 271546
 * @author Carlos Gerardo Cedillo Alcántar 271488
 */
public class AnalizadorProduccionesGramaticales {

    private static final int CONCAT = '&';
    private static final int ALTER = '|';
    private static final int FIN_SENT = ';';
    private static final int EOF = '.';
    private static final int PAR_IZQ = '(';
    private static final int PAR_DER = ')';
    private static final int COR_IZQ = '[';
    private static final int COR_DER = ']';
    private static final int LLAVE_IZQ = '{';
    private static final int LLAVE_DER = '}';
    private static final int VARIABLE = 600;
    private static final int TERMINAL = 700;
    private static final int ASIGNACION = 800;

    private Integer linea = 1;
    private Integer caracter = 0;
    private String salida = "";
    private String entrada = "";
    private Token currentToken;
    private File archivoSalida;

    /**
     * Constructor del analizador de producciones gramaticales
     *
     * @param pathArchivoFuente direccion del archivo fuente
     * @param pathArchivoSalida direccion y nombre del archivo de salida
     * @throws IOException cuando ocurre un error al leer o escribir archivos
     */
    public AnalizadorProduccionesGramaticales(String pathArchivoFuente, String pathArchivoSalida) throws IOException {
        File archivoEntrada = new File(pathArchivoFuente);
        File archivoSalida = new File(pathArchivoSalida);
        FileWriter archivoFinal = null;
        FileReader archivoFuente = null;
        String entrada = "";
        int character;

        if (!archivoEntrada.exists()) {
            throw new Error(String.format("El archivo %s no existe", pathArchivoFuente));
        }

        if (archivoSalida.exists()) {
            archivoSalida.delete();
        }

        archivoSalida.createNewFile();
        archivoFuente = new FileReader(archivoEntrada);
        character = archivoFuente.read();

        while (character != -1) {
            entrada = String.format("%s%s", entrada, (char) character);
            character = archivoFuente.read();
        }

        this.archivoSalida = archivoSalida;
        limpiar(entrada);
        archivoFuente.close();
    }

    /**
     * Escribe la salida del compilador por el archivo creado en el constructor
     * y lo cierra.
     *
     * @throws IOException en caso de que algo malo pase
     */
    public void salida() throws IOException {
        FileWriter archivo = new FileWriter(this.archivoSalida);
        archivo.write(this.salida);
        archivo.close();
    }

    //TODO JavaDoc de la funcion parser
    public void parser() throws IOException {
        this.currentToken = lex();
        prog();
        if (currentToken.getToken() == EOF) {
            System.out.println(String.format("\n Resultado: %s \n El programa a corrido correctamente", this.salida));
        }
        salida();
    }

    //TODO JavaDoc de la funcion prog
    public void prog() {

        conjuProds();
        if (this.currentToken.getToken() != FIN_SENT) {
            throw new Error(String.format("\nerror de sintaxis se esperaba '%s'", (char) FIN_SENT));
        }
    }

    //TODO JavaDoc de la funcion conjProds
    public void conjuProds() {
        prod();
    }

    /**
     * Hace referencia a la expreson gramarical prod, se encarga de que las
     * sentencias se compongan de la estructura variable asignacion exp;
     *
     * @throws Error en caso de que no se cumpla
     */
    public void prod() {
        if (this.currentToken.getToken() != VARIABLE) {
            throw new Error(String.format(
                    "Error de sintaxis en la linea %s: se esperaba una variable",
                    this.linea));
        }
        this.salida = String.format("%s%s", this.salida, this.currentToken.getLexema());
        this.currentToken = lex();
        if (this.currentToken.getToken() != ASIGNACION) {
            throw new Error(String.format(
                    "Error de sintaxis en la linea %s: se esperaba el operador de asignacion",
                    this.linea));
        }
        this.currentToken = lex();
        exp();
        if (this.currentToken.getToken() != FIN_SENT) {
            throw new Error(String.format(
                    "Error de sintaxis en la linea %s: se esperaba %s",
                    this.linea, (char) FIN_SENT));
        }
        this.salida = String.format("%s%s", this.salida, "::=");
    }

    /**
     * Hace referencia a la expresion gramatical exp, se encarga de las
     * expresiones de alternacion representadas por el simbolo | delegando la
     * mayor parte a la funcion term
     */
    public void exp() {
        term();
        while (this.currentToken.getToken() == ALTER) {
            this.currentToken = lex();
            term();
            this.salida = String.format("%s%s", this.salida, (char) ALTER);
        }
    }

    /**
     * Hace referencia a la expresion gramatical term, se encarga de las
     * expresiones de concatenacion representadas por el simbolo & delegando la
     * mayor parte a la funcion factor
     */
    public void term() {
        factor();
        while (this.currentToken.getToken() == CONCAT) {
            this.salida = String.format("%s%s", this.salida, (char) CONCAT);
            this.currentToken = lex();
            factor();
        }
    }

    /**
     * Hace referencia a la exprecion gramatica factor, se encarga de verificar
     * si son expreciones entre [] o {}, en otro caso llama a la funcio primario
     *
     * @throws Error cuando falta el cierre de los [] o {}
     */
    public void factor() {
        if (this.currentToken.getToken() != LLAVE_IZQ
                && this.currentToken.getToken() != COR_IZQ) {
            primario();
        } else {
            if (this.currentToken.getToken() == LLAVE_IZQ) {
                this.currentToken = lex();
                exp();
                if (this.currentToken.getToken() != LLAVE_DER) {
                    throw new Error(String.format(
                            "Error de sintaxis en la linea %s: se esperaba",
                            this.linea, (char) LLAVE_DER));
                }
                this.salida = String.format("%s%s", this.salida,
                        (char) LLAVE_DER);
            } else {
                this.currentToken = lex();
                exp();
                if (this.currentToken.getToken() != COR_DER) {
                    throw new Error(String.format(
                            "Error de sintaxis en la linea %s: se esperaba",
                            this.linea, (char) COR_DER));
                }
                this.salida = String.format("%s%s", this.salida,
                        (char) COR_DER);
            }
            this.currentToken = lex();
        }
    }

    /**
     * Hace referencia a la exprecion gramatical primario, se encarga de que los
     * token resividos sean un terminal, una variable o una expresion entre ()
     *
     * @throws Error en caso contrario, especificando que se esperaba
     */
    public void primario() {
        if (this.currentToken.getToken() == TERMINAL) {
            this.salida = String.format("%s%s", this.salida,
                    this.currentToken.getLexema());
        } else if (this.currentToken.getToken() == VARIABLE) {
            this.salida = String.format("%s%s", this.salida,
                    this.currentToken.getLexema());
        } else if (this.currentToken.getToken() == PAR_IZQ) {
            this.currentToken = lex();
            exp();
            if (this.currentToken.getToken() != PAR_DER) {
                throw new Error(String.format(
                        "\nError de Sintaxis en linea %s: se esperaba %s",
                        this.linea, (char) PAR_DER));
            }
        } else {
            throw new Error(String.format(
                    "\nError de Sintaxis en la linea %s: se esperana ID o Terminal",
                    this.linea));
        }
        this.currentToken = lex();
    }

    //TODO JavaDoc de la funcion esVariableTerminal
    private Boolean esVariableTerminal(Token token) {
        return token.getToken() == VARIABLE || token.getToken() == TERMINAL;
    }

    /**
     * Elimina los èspacios en blanco, tabuladores y saltos de linea del codigo
     * fuente
     * @param entrada el codigo fuente de la funcion
     */
    private void limpiar(String entrada) {
        char j;
        for (int i = 0; i < entrada.length(); i++) {
            j = entrada.charAt(i);
            if (j != ' ' && j != '\n' && j != '\t') {
                this.entrada = String.format("%s%s", this.entrada, j);
            }
        }
    }

    /**
     * Analiza el texto fuente, si corresponde al lexema de una variable, 
     * devuelve el objeto, si no corresponde devuelve null, si esta mal escrito 
     * el lexema levanta un error.
     * @return Token con el lexema de una variable
     */
    public Token variable() {
        Token token = null;
        int i = 0;
        char a = this.entrada.charAt(this.caracter + i);
        if (a == '<') {
            i = 1;
            a = this.entrada.charAt(this.caracter + i);
            if (!Character.isAlphabetic(a)) {
                throw new Error(String.format(
                        "Error lexico en la linea %s: Todas las variables empiezan con letra despues del <",
                        this.linea));
            }
            try {
                i = 2;
                a = this.entrada.charAt(this.caracter + i);
                while (a != '>') {
                    if (Character.isAlphabetic(a) || Character.isDigit(a)) {
                        i += 1;
                        a = this.entrada.charAt(this.caracter + i);
                    } else {
                        throw new Error(String.format(
                                "Error lexico en la linea %s: el catacter '%s' es ilegal dentro de una variable",
                                this.linea, a));
                    }
                }
            } catch (StringIndexOutOfBoundsException ex) {
                throw new Error(String.format(
                        "Error lexico en la linea %s: Olvido  el caracter >",
                        this.linea));
            }
            token = new Token(this.linea, VARIABLE, this.entrada.substring(this.caracter, this.caracter + i + 1));
            this.caracter += i + 1;

        }
        return token;
    }

    /**
     * Analiza el texto fuente, si corresponde al lexema de un terminal, 
     * devuelve el objeto, si no corresponde devuelve null, si esta mal escrito 
     * el lexema levanta un error.
     * @return Token con el lexema de un terminal
     */
    public Token terminal() {
        Token token = null;
        int i = 0;
        char a = this.entrada.charAt(this.caracter + i);
        if(a == '\''){
            try{
                i = 1;
                a = this.entrada.charAt(this.caracter + i);
                while (a != '\'') {
                    i += 1;
                    a = this.entrada.charAt(this.caracter + i);
                }
            }
            catch (StringIndexOutOfBoundsException ex) {
                throw new Error(String.format(
                        "Error lexico en la linea %s: Olvido  el caracter \" ' \"",
                        this.linea));
            }
            token = new Token(this.linea, TERMINAL, this.entrada.substring(this.caracter, this.caracter + i + 1));
            this.caracter += i + 1;
        }
        return token;
    }

    /**
     * Analiza el texto fuente, si corresponde al lexema de asignacion, devuelve
     * el objeto, si no corresponde devuelve null, si esta mal escrito el
     * lexema de asignacion levanta un error.
     * @return Token con el lexema de asignacion
     */
    public Token asignacion() {
        Token token = null;
        if (this.entrada.charAt(this.caracter) == ':'){
            try{
                this.caracter += 1;
                if (this.entrada.charAt(this.caracter) != ':') {
                    throw new Exception();
                }
                this.caracter += 1;
                if (this.entrada.charAt(this.caracter) != '=') {
                    throw new Exception();
                }
                token = new Token(this.linea, ASIGNACION, "::=");
                this.caracter += 1;
            }catch (Exception ex) {
                throw new Error(String.format(
                        "Error lexico en la linea %s: se esperaba el caracter \"::=\" completo",
                        this.linea));
            }
        }
        return token;
    }

    /**
     * Esta funcion se encarga del analisis lexico, deleg funciones a variable,
     * terminal y asignacion, que son lexemas compuestos
     * @see  Token
     * @return Token del siguiente token
     */
    public Token lex() {
        Token token = null;
        if (this.caracter < this.entrada.length()) {
            token = variable();
            if (token == null) {
                token = terminal();
                if (token == null) {
                    token = asignacion();
                    if (token == null) {
                        int tokenSimple = this.entrada.charAt(this.caracter);
                        switch (tokenSimple) {
                            case FIN_SENT:

                                token = new Token(this.linea, FIN_SENT, String.format("%s", (char) tokenSimple));
                                linea++;
                                break;
                            case CONCAT:
                                token = new Token(this.linea, CONCAT, String.format("%s", (char) tokenSimple));

                                break;
                            case ALTER:
                                token = new Token(this.linea, ALTER, String.format("%s", (char) tokenSimple));

                                break;
                            case PAR_IZQ:
                                token = new Token(this.linea, PAR_IZQ, String.format("%s", (char) tokenSimple));

                                break;
                            case PAR_DER:
                                token = new Token(this.linea, PAR_DER, String.format("%s", (char) tokenSimple));

                                break;
                            case COR_IZQ:
                                token = new Token(this.linea, COR_IZQ, String.format("%s", (char) tokenSimple));

                                break;
                            case COR_DER:
                                token = new Token(this.linea, COR_DER, String.format("%s", (char) tokenSimple));

                                break;
                            case LLAVE_IZQ:
                                token = new Token(this.linea, LLAVE_IZQ, String.format("%s", (char) tokenSimple));

                                break;
                            case LLAVE_DER:
                                token = new Token(this.linea, LLAVE_DER, String.format("%s", (char) tokenSimple));

                                break;
                            default:
                                throw new Error(String.format(
                                        "Error de lexico en la linea %s: el caracter '%s' no esta en el alfabeto",
                                        this.linea,
                                        (char)tokenSimple));
                        }
                        this.caracter += 1;

                    }
                }
            }
        } else {
            token = new Token(this.linea, EOF, ",");

        }
        return token;
    }

    public static void main(String[] args) throws IOException {
        String rutaE = "entrada.txt";
        String rutaS = "salida.txt";
        String codigo
                = "<Entero> ::={{['+'|'-']&<variable1>}&(['+'|'-'])&{<variable2>}}&{['+'|'-']&<variable3>};";
        File entrada = new File(rutaE);
        FileWriter archivoEntrada = new FileWriter(entrada);
        archivoEntrada.write(codigo);
        archivoEntrada.close();
        AnalizadorProduccionesGramaticales analizador
                = new AnalizadorProduccionesGramaticales(rutaE, rutaS);
        analizador.parser();
    }
}
